﻿/*

The MIT License

Copyright (c) 2010 Cartesian Analytics, Inc. 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Pipra.Uom
{
    /// <summary>
    /// A conversion factor entry which stores the faction used for some unit conversion operations.
    /// </summary>
    public class ConversionFactorEntry
        : IEquatable<ConversionFactorEntry>
    {
        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="a">A conversion factor entry.</param>
        /// <param name="b">A conversion factor entry.</param>
        /// <returns>Thre when equal.</returns>
        public static bool operator ==(ConversionFactorEntry a, ConversionFactorEntry b)
        {
            return ReferenceEquals(a, b) || (!ReferenceEquals(null, a) && a.Equals(b));
        }
        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="a">A conversion factor entry.</param>
        /// <param name="b">A conversion factor entry.</param>
        /// <returns>True when not equal.</returns>
        public static bool operator !=(ConversionFactorEntry a, ConversionFactorEntry b)
        {
            return !ReferenceEquals(a, b) && (ReferenceEquals(null, a) || !a.Equals(b));
        }

        /// <summary>
        /// A faction equal to 1/1 .
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public static readonly ConversionFactorEntry Unity = new ConversionFactorEntry(1);

        /// <summary>
        /// Constructs a new inverted conversion entry for converting in the oposite direction.
        /// </summary>
        /// <param name="entry">The entry to generate a multiplicitive inversion for.</param>
        /// <returns>The multiplicitive inverse of the given <paramref name="entry"/>.</returns>
        /// <remarks>
        /// The resulting entry will accumulte one operation count.
        /// </remarks>
        public static ConversionFactorEntry GetInverted(ConversionFactorEntry entry)
        {
            return new ConversionFactorEntry(
                entry.Denominator,
                entry.Numerator,
                entry.OpCount
            );
        }

        /// <summary>
        /// Creates a squared conversion factor entry.
        /// </summary>
        /// <param name="entry">The entry to square.</param>
        /// <returns>A squared entry.</returns>
        public static ConversionFactorEntry GetSquared(ConversionFactorEntry entry)
        {
            return new ConversionFactorEntry(
                entry.Numerator * entry.Numerator,
                entry.Denominator * entry.Denominator,
                entry.OpCount + 1
            );
        }
        /// <summary>
        /// Creates a cubed conversion factor entry.
        /// </summary>
        /// <param name="entry">The entry to cube.</param>
        /// <returns>A cubed entry.</returns>
        public static ConversionFactorEntry GetCubed(ConversionFactorEntry entry)
        {
            return new ConversionFactorEntry(
                entry.Numerator * entry.Numerator * entry.Numerator,
                entry.Denominator * entry.Denominator * entry.Denominator,
                entry.OpCount + 2
            );
        }

        private static void CrossEliminate(List<ConversionFactorEntry> entries)
        {
            for (int i = 0; i < entries.Count; i++)
            {
                for (int j = i + 1; j < entries.Count; j++)
                {
                    if (
                        entries[i].Numerator == entries[j].Denominator
                        &&
                        entries[i].Denominator == entries[j].Numerator
                    )
                    {
                        entries.RemoveAt(j);
                        entries.RemoveAt(i);
                        i--;
                        break;
                    }
                }
            }
        }

        private static void CrossCombine(List<ConversionFactorEntry> entries)
        {
            for (int i = 0; i < entries.Count; i++)
            {
                for (int j = i + 1; j < entries.Count; j++)
                {
                    if (entries[i].Numerator == entries[j].Denominator)
                    {
                        entries[i] = new ConversionFactorEntry(
                            entries[j].Numerator,
                            entries[i].Denominator
                        );
                        entries.RemoveAt(j);
                        i--;
                        break;
                    }
                    if (entries[i].Denominator == entries[j].Numerator)
                    {
                        entries[i] = new ConversionFactorEntry(
                            entries[i].Numerator,
                            entries[j].Denominator
                        );
                        entries.RemoveAt(j);
                        i--;
                        break;
                    }
                }
            }
        }

        private static ConversionFactorEntry Combine(List<ConversionFactorEntry> entries)
        {
            double numerator = 1;
            double denominator = 1;
            foreach (ConversionFactorEntry entry in entries)
            {
                numerator *= entry.Numerator;
                denominator *= entry.Denominator;
                double gcd;
                if (
                    System.Math.Floor(numerator) == numerator
                    && System.Math.Floor(denominator) == denominator
                    && TryFindGcd(numerator, denominator, out gcd)
                    )
                {
                    numerator /= gcd;
                    denominator /= gcd;
                }
            }
            return (
                numerator.Equals(denominator)
                ? Unity
                : new ConversionFactorEntry(
                    numerator,
                    denominator,
                    entries.Sum(e => e.OpCount) + entries.Count - 1
                )
            );
        }

        /// <summary>
        /// Reduces the specified entries into a single entry.
        /// </summary>
        /// <param name="entriesEnumerable">The entries to reduce.</param>
        /// <returns>A reduced entry.</returns>
        public static ConversionFactorEntry Reduce(IEnumerable<ConversionFactorEntry> entriesEnumerable)
        {
            List<ConversionFactorEntry> entries =
                null == entriesEnumerable
                ? new List<ConversionFactorEntry>()
                : new List<ConversionFactorEntry>(entriesEnumerable)
            ;

            CrossEliminate(entries);
            CrossCombine(entries);

            return entries.Count == 0 ? Unity : Combine(entries);
        }

        private static bool TryFindGcd(double a, double b, out double gcd)
        {
            int res;
            if (TryFindGcd((int)a, (int)b, out res))
            {
                gcd = res;
                return true;
            }
            gcd = 1;
            return false;
        }

        private static bool TryFindGcd(int a, int b, out int gcd)
        {
            if (a < 2 || b < 2)
            {
                gcd = 1;
                return false;
            }
            while (true)
            {
                if (a > b)
                {
                    a %= b;
                    if (0 == a)
                    {
                        break;
                    }
                }
                else
                {
                    b %= a;
                    if (0 == b)
                    {
                        break;
                    }
                }
            }
            gcd = a == 0 ? b : a;
            return gcd > 1;
        }

        /// <summary>
        /// The numerator value used to perform multiplication.
        /// </summary>
        public readonly double Numerator;
        /// <summary>
        /// The denominator value used to perform division.
        /// </summary>
        public readonly double Denominator;
        /// <summary>
        /// The count of operations required to reach these values.
        /// </summary>
        /// <remarks>
        /// This field is recorded in order to calculate derrived conversion entries using minimal operations.
        /// </remarks>
        public readonly int OpCount;


        /// <summary>
        /// Creates a conversion factor.
        /// </summary>
        /// <param name="numerator">The factor value to multiply by.</param>
        public ConversionFactorEntry(int numerator) : this((double)numerator) { }
        /// <summary>
        /// Creates a conversion factor.
        /// </summary>
        /// <param name="numerator">The factor value to multiply by.</param>
        /// <param name="denominator">The divisor value to divide by.</param>
        public ConversionFactorEntry(int numerator, int denominator) : this(numerator, (double)denominator) { }
        /// <summary>
        /// Creates a conversion factor.
        /// </summary>
        /// <param name="numerator">The factor value to multiply by.</param>
        public ConversionFactorEntry(double numerator) : this(numerator, 1) { }
        /// <summary>
        /// Creates a conversion factor.
        /// </summary>
        /// <param name="numerator">The factor value to multiply by.</param>
        /// <param name="denominator">The divisor value to divide by.</param>
        public ConversionFactorEntry(double numerator, double denominator)
            : this(
                numerator,
                denominator,
                (
                    numerator == denominator
                    || 1 == numerator
                    || 1 == denominator
                    ) ? 0 : 1
                )
        { }

        private ConversionFactorEntry(double numerator, double denominator, int opCount)
        {
            Numerator = numerator;
            Denominator = denominator;
            OpCount = opCount;
        }

        /// <summary>
        /// Generates a single factor that would be used to multiply against a value to convert it to another unit.
        /// </summary>
        /// <returns>A factor value that can be used to perform a conversion.</returns>
        public double GetFactor()
        {
            return (
                1 == Denominator
                ? Numerator
                : Numerator / Denominator
            );
        }

        public bool Equals(ConversionFactorEntry other)
        {
            return !ReferenceEquals(null, other)
                && Numerator == other.Numerator
                && Denominator == other.Denominator
                && OpCount == other.OpCount
            ;
        }

        public override bool Equals(object obj)
        {
            return null != obj
                && (
                    (obj is ConversionFactorEntry && Equals(obj as ConversionFactorEntry))
                )
            ;
        }

        public override int GetHashCode()
        {
            return Numerator.GetHashCode();
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return String.Concat("f: ", Numerator.ToString(), '/', Denominator.ToString(), ", op: ", OpCount.ToString());
        }


        
    }
}
